/*
 * Copyright (c) 2011-2012 Alexander Dubu
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * o  Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * o  Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * o  Neither the name Axil nor the names of its contributors may be used to
 *    endorse or promote products derived from this software without specific
 *    prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package axil.engine;

import axil.api.AxilType;
import axil.api.Configuration;
import axil.definition.*;
import axil.etc.PropertyFile;
import axil.framework.resource.ResourceManager;

import java.io.File;
import java.util.*;

import static axil.framework.Functions.*;


/**
 * The actual configuration object implementing the Axil native API.
 */
public class Config implements Configuration {
	/**
	 * An enumerated type of various options that can be specified in the
	 * configuration. These options are not part of the official Axil Native API
	 * but are unique to this specific implementation. These must be specified
	 * in the configuration file for the runtime itself (compiler.cfg).
	 */
	public enum Option {
		trace("trace-context-paths", false),
		nulls("null-context-as-nil", false);

		private String key;
		private boolean defval;

		private Option(String key, boolean defval) {
			this.key = key;
			this.defval = defval;
		}

		public String key() 	{
			return key;
		}
		public boolean defval() {
			return defval;
		}
	}

	public static final int DISABLED = -1;

	private int memory;
	private int output;
	private int duration;
	private boolean sealed;
	private List<String> mods;
	private boolean[] options;
	private HashMap<String,Object> extras;

	private transient Boolean dates;
	private transient Boolean money;
	private transient ArrayList<Module> modules;
	private transient Map<String,TypeDefinition> types;
	private transient Map<String,FunctionDefinition> functions;
	private transient Map<String,ConstantDefinition> constants;
	private transient Map<String,OperatorDefinition> operators;


	private Config() {
		this.memory = 32000;
		this.output = 8000;
		this.duration = DISABLED;
		this.sealed = false;
		this.mods = new ArrayList<String>();
		this.extras = new HashMap<String, Object>();
		this.money = null;
		this.dates = null;
		this.options = new boolean[Option.values().length];
		for (Option opt : Option.values()) {
			options[opt.ordinal()] = opt.defval();
		}
	}


	public Config(String name) {
		this(new PropertyFile(ResourceManager.instance().reader(name)));
	}


	public Config(File file) {
		this(new PropertyFile(file));
	}


	private Config(PropertyFile props) {
		this();
		this.memory = props.integer("max-memory", this.memory);
		this.output = props.integer("max-output", this.output);
		this.duration = props.integer("max-duration", this.duration);
		mods.addAll(props.list("modules"));

		for (Option opt : Option.values()) {
			options[opt.ordinal()] = props.bool(opt.key(), opt.defval());
		}
	}


	/**
	 * Create a configuration specifically for compiling the code contained
	 * within a module. This configuration contains only this module and the
	 * modules it depends upon.
	 */
	public Config(Module module) {
		this();
		this.mods = list(walk(module, new HashSet<String>()));
	}


	private Config(Config other) {
		other.seal();
		this.memory  = other.memory;
		this.output = other.output;
		this.duration = other.duration;
		this.extras = (HashMap<String,Object>)other.extras.clone();
		this.mods = new ArrayList<String>();
		this.options = Arrays.copyOf(other.options, other.options.length);
		this.sealed = false;
	}


	/**
	 * Specify the maximum amount of memory that Axil is allowed to consume
	 * when running a script. This value is per thread, not for the entire
	 * Axil runtime. If a script attempts to consume more than this amount, then
	 * the script will be terminated and an exception returned to the caller
	 * indicating the defect in the script.
	 * <p/>
	 * This operation can only be performed before the configuration is put into
	 * use. Once a context has been created associated with this configuration,
	 * then the configuration is sealed (immutable). Any attempt to invoke this
	 * method after that time will result in an exception.
	 *
	 * @param bytes
	 * 	The number of bytes that are the maximum heap space Axil will consume
	 * 	when running.
	 */
	public void memory(int bytes) {
		check();
		assert memory > 1000;
		this.memory = bytes;
	}


	/**
	 * Specify the maximum amount of output that can be generated into the
	 * console by the script. If a script attempts to generate more than this
	 * amount, then the script will be terminated and an exception returned to
	 * the caller indicating the defect in the script. If this is in explicitly
	 * invoked, then a reasonable limit will be automatically applied.
	 * <p/>
	 * This operation can only be performed before the configuration is put into
	 * use. Once a context has been created associated with this configuration,
	 * then the configuration is sealed (immutable). Any attempt to invoke this
	 * method after that time will result in an exception.
	 *
	 * @param chars
	 * 	The number of characters that can be output. A value less than or equal
	 * 	to zero indicates that no limit is being imposed on output. Note that
	 * 	allowing unlimited output is intentionally disabling an Axil safety
	 * 	mechanism.
	 */
	public void output(int chars) {
		check();
		this.output = chars;
	}


	/**
	 * Get the maximum number of characters that can be output by a print()
	 * function before raising an error.
	 *
	 * @return
	 * 	The number of characters that can be output. A value less than or equal
	 * 	to zero indicates that no limit is being imposed on output.
	 */
	public int output() {
		return output;
	}


	/**
	 * Set the maximum duration that a script is allowed to execute before
	 * forcibly terminating the script. In general, long-running scripts are
	 * not an issue in Axil, and the functional nature of the execution means
	 * that the memory and output limits are typically sufficient to ensure safe
	 * operation. However, application-specific types or functions may introduce
	 * opportunities for runaway scripts that neither consume too much memory
	 * nor exceed stack limits. The standard library provided with Axil is
	 * immune to these effects. Therefore, this feature is disabled by default,
	 * allowing scripts to run for an extended period of time.
	 * <p/>
	 * This operation can only be performed before the configuration is put into
	 * use. Once a context has been created associated with this configuration,
	 * then the configuration is sealed (immutable). Any attempt to invoke this
	 * method after that time will result in an exception.
	 * <p/>
	 * Finally, note that there is significant overhead associated with
	 * enabling this feature. This feature introduces synchronization and related
	 * threading overhead to an otherwise re-entrant execution mechanism.
	 *
	 * @param milliseconds
	 * 	The maximum number of milliseconds a script is allowed to execute. A
	 * 	value of -1 is set to indicate this feature is disabled.
	 */
	public void duration(int milliseconds) {
		check();
		assert (milliseconds == DISABLED) || (milliseconds > 0);
		this.duration = milliseconds;
	}


	/**
	 * Indicate that the types, constants, functions and operators defined in
	 * the given module are accessible to script compiled and executed under
	 * this context. By default, only the "core" module is available. All other
	 * modules must be explicitly specified.
	 * <p/>
	 * This operation can only be performed before the configuration is put into
	 * use. Once a context has been created associated with this configuration,
	 * then the configuration is sealed (immutable). Any attempt to invoke this
	 * method after that time will result in an exception.
	 *
	 * @param module
	 */
	public void use(String module) {
		check();
		if (! mods.contains(module)) {
			mods.add(module);
		}
	}


	/**
	 * Add all of the modules listed to this configuration. Any duplicates are
	 * automatically ignored.
	 */
	public void use(Set<String> modules) {
		check();
		for (String mod : modules) {
			use(mod);
		}
	}


	/**
	 * Put an object that is specific to the host application into this
	 * configuration. If an object already exists with that key, then that
	 * object is replaced with this one. This method is provided to allow the
	 * host application to make its own configuration visible to objects like
	 * source adapters.
	 * <p/>
	 * This operation can only be performed before the configuration is put into
	 * use. Once a context has been created associated with this configuration,
	 * then the configuration is sealed (immutable). Any attempt to invoke this
	 * method after that time will result in an exception.
	 *
	 * @param key
	 * 	The host application key, which cannot be null or empty.
	 *
	 * @param value
	 * 	The host application object, which cannot be null.
	 *
	 * @see axil.api.extend.SourceAdapter
	 */
	public void put(String key, Object value) {
		check();
		if (empty(key)) {
			throw abort("An empty key cannot be  used.");
		}
		if (value == null) {

			throw abort(
			    "A null object cannot be placed into the configuration.");
		}
		extras.put(key, value);
	}


	/**
	 * Get an object that is specific to the host application into this
	 * configuration. If there is no object associated with that key, then an
	 * exception is thrown.
	 *
	 * @param key
	 * 	The host application key, which cannot be null or empty.
	 *
	 * @return
	 * 	Returns the value from the last put() operation for this key. The
	 * 	object returned is never null.
	 */
	public Object get(String key) {
		Object v = extras.get(key);
		if (v == null) {
			throw abort("An object with that key was never placed into the " +
			            "configuration.", nv("key", key));
		}
		return v;
	}


	/**
	 * Return a clone of this configuration suitable for modification. This
	 * method can be safely invoked at any time.
	 *
	 * @return
	 * 	Returns a clone of this configuration, with the clone being completely
	 * 	isolated. No modifications to the returned object have side effects
	 * 	to this object.
	 */
	public Configuration reconfigure() {
		return new Config(this);
	}


	/**
	 * Walk through a module and all of its dependencies, returning a set
	 * containing the name of this module and all of the modules upon which it
	 * depends.
	 */
	private Set<String> walk(Module module, Set<String> modules) {
		String ident = module.identity();
		if (! modules.contains(ident)) {
			modules.add(ident);
			for (String depend : module.dependencies()) {
				Module mod = Repository.instance().module(depend);
				walk(mod, modules);
			}
		}
		return modules;
	}


	/**
	 * Return a list of all of the modules defined in this configuration. The
	 * list includes all of the modules automatically loaded because of
	 * dependencies declared in the modules explicitly specified.
	 */
	public List<Module> modules() {
		if (modules == null) {
			Repository repo = Repository.instance();
			Set<String> depends = new HashSet<String>();
			for (String m : mods) {
				walk(repo.module(m), depends);
			}
			ArrayList<Module> list = new ArrayList<Module>(depends.size());
			for (String m : depends) {
				list.add(repo.module(m));
			}
			modules = list;
		}
		return modules;
	}


	/**
	 * Return the function definition that matches the given identifier, or
	 * return null if no such function is defined or visible within this
	 * configuration.
	 */
	public FunctionDefinition function(String ident) {
		assert sealed;
		if (functions == null) {
			Map<String,FunctionDefinition> map =
			    new HashMap<String, FunctionDefinition>();
			for (Module module : modules()) {
				for (FunctionDefinition fd : module.functions()) {
					map.put(fd.identity(), fd);
				}
			}
			functions = map;
		}
		return functions.get(ident);
	}


	/**
	 * Return the constant definition that matches the given identifier, or
	 * return null if no such constant is defined or visible within this
	 * configuration.
	 */
	public ConstantDefinition constant(String ident) {
		assert sealed;
		if (constants == null) {
			Map<String,ConstantDefinition> map =
			    new HashMap<String, ConstantDefinition>();
			for (Module module : modules()) {
				for (ConstantDefinition cd : module.constants()) {
					map.put(cd.identity(), cd);
				}
			}
			constants = map;
		}
		return constants.get(ident);
	}


	/**
	 * Return the operator definition that matches the given identifier, or
	 * return null if no such operator is defined or visible within this
	 * configuration.
	 */
	public OperatorDefinition operator(String ident) {
		assert sealed;
		if (operators == null) {
			Map<String,OperatorDefinition> map =
			    new HashMap<String, OperatorDefinition>();
			for (Module module : modules()) {
				for (OperatorDefinition op : module.operators()) {
					put(map, op.symbol(), op);
					for (String alias : op.aliases()) {
						put(map, alias, op);
					}
				}
			}
			operators = map;
		}
		return operators.get(ident);
	}


	private void put(Map<String,OperatorDefinition> map, String key, OperatorDefinition def) {
		OperatorDefinition existing = map.put(key, def);
		if (existing != null) {
			throw abort(
			    "The character sequence for an operator has been bound to " +
			    "more than one operator.",
			    nv("chars", key),
			    nv("bound-to", existing.symbol()),
			    nv("and", def.symbol()));
		}
	}


	private Map<String,TypeDefinition> types() {
		if (types == null) {
			Map<String,TypeDefinition> map = new HashMap<String, TypeDefinition>();
			for (Module m : modules()) {
				for (TypeDefinition type : m.types()) {
					map.put(type.identity(), type);
				}
			}
			types = map;
		}
		return types;
	}


	/**
	 * Return the Axil type object matching the given identifier. If the type
	 * is not a known type, or the type is not accessible via this configuration
	 * (the module is which the type resides was not included), then a null is
	 * returned.
	 */
	public AxilType type(String ident) {
		TypeDefinition def = types().get(ident);
		return def == null ? null : def.type();
	}


	public boolean allows(AxilType type) {
		return types().containsKey(type.identity());
	}


	/**
	 * Tell if the special mode in the parser that supports date and time
	 * literals should be activated. It is only activated when the "time" module
	 * is part of this configuration.
	 */
	public boolean dates() {
		if (dates == null) {
			dates = Boolean.FALSE;
			for (Module m : modules()) {
				if (m.identity().equals("time")) {
					dates = Boolean.TRUE;
					break;
				}
			}
		}
		return dates;
	}


	/**
	 * Tell if the special mode in the parser that supports monetary literals
	 * should be activated. It is only activated when the "money" module is
	 * part of this configuration.
	 */
	public boolean monies() {
		if (money == null) {
			money = Boolean.FALSE;
			for (Module m : modules()) {
				if (m.identity().equals("money")) {
					money = Boolean.TRUE;
					break;
				}
			}
		}
		return money;
	}


	// TODO: I think we can do the same as above for fractions.


	/**
	 * Tell if the given configuration option has been enabled. The option given
	 * cannot be null.
	 */
	public boolean enabled(Option option) {
		return options[option.ordinal()];
	}


	private void check() {
		if (sealed) {
			throw abort(
			    "This configuration has already been put into use and can no " +
			    "longer be altered.");
		}
	}


	/**
	 * Seal this configuration from further modification, preparing it for use in
	 * script execution. The object will throw an exception if any attempt is
	 * made to modify the object (such as via the locale() method) once it has
	 * been sealed.
	 *
	 * @return
	 * 	This object is returned.
	 */
	public Config seal() {
		if (! sealed) {
			synchronized (this) {
				modules();
				sealed = true;
			}
		}
		return this;
	}


	/**
	 * Return a string representation of this object. The string returned is
	 * never null. This is intended for diagnostic purposes only.
	 */
	public String toString() {
		StringBuilder b = new StringBuilder();
		b.append("Memory: ");
		b.append(memory);
		b.append("; Max Output: " );
		b.append(output);
		b.append("; Max Duration: ");
		b.append(duration);
		b.append("; Modules: ");
		b.append(separated(mods));
		return b.toString();
	}
}
